#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/shape.h>
#include <X11/xpm.h>
#include <dirent.h>
#include <errno.h>
#include <freetype2/ft2build.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include FT_FREETYPE_H
#include FT_GLYPH_H

#define VERSION "v0.13"
#define ABS(a) ((a) < 0 ? -(a) : (a))
#define CLAMP(v, l, h) ((v) < (l) ? (l) : ((v) > (h) ? (h) : (v)))
#define DEFAULT_PET_REFRESH 24
#define DEFAULT_ORIGINAL_SIZE 64
#define DEFAULT_WANDER_MIN_WAIT 16000
#define DEFAULT_WANDER_MAX_WAIT 32000
#define DEFAULT_WANDER_MARGIN 100
#define DEFAULT_SLEEP_DELAY 5000
#define DEFAULT_HAPPY_DURATION 3000
#define DEFAULT_SPEECH_DURATION 5000
#define BUBBLE_PADDING 24
#define TEXT_LEFT_PAD 16
#define TEXT_TOP_PAD 16
#define HELPER_BUBBLE_SCALE 2 // 气泡放大2倍（可改为3调整更大）
#define MAX_BUBBLE_WIDTH 800  // 限制最大宽度，避免超出屏幕
#define MAX_BUBBLE_HEIGHT 600 // 限制最大高度

#define CONFIG_DIR "~/.config/xpet"
#define CONFIG_PATH CONFIG_DIR "/config"

// Forward declarations (fixed implicit declaration)
void show_speech_bubble(const char *s);

typedef enum {
  FUNC_TOGGLE_CHASE,
  FUNC_TOGGLE_FREEZE,
  FUNC_QUIT,
  FUNC_PRINT_HELLOWORLD,
  FUNC_HELPER_INFO,
  FUNC_HOT_RELOAD, // 新增热重载枚举值
  FUNC_UNKNOWN,
} KeyFunc;

enum state {
  SLEEPING,
  IDLE,
  N,
  S,
  E,
  W,
  NW,
  NE,
  SW,
  SE,
  DRAGGED,
  HAPPY,
  STATE_LAST
};

struct frame {
  Pixmap pix;
  Pixmap mask;
  int duration;
};
struct animation {
  struct frame *frames;
  const char *name;
  int n_frames;
  Bool loop;
};
struct mouse {
  int x;
  int y;
};
struct keybind {
  KeySym sym;
  unsigned long mask;
  KeyFunc func;
  char *desc; // For helper info
};

typedef struct {
  char **phrases;
  int phrase_count;
  int scale_factor;
  int pet_speed;
  int frame_duration;
  char *pet_asset_dir;
  char *font_path;
  int font_size;
  struct keybind *keybinds;
  int keybind_count;
  char *click_audio_to_play;
  Bool enable_audio;
  Bool enable_random_audio;
  char *random_audio_folder;
} Config;

struct pet {
  Window window;
  Bool chasing;
  Bool frozen;
  enum state state;
  int x;
  int y;
  double subpixel_x;
  double subpixel_y;
  struct animation *current_animation;
  int current_frame;
  long frame_time;
  int target_x;
  int target_y;
  long wander_wait;
  long frozen_time;
  enum state previous_state;
  long happy_time;
  Bool dragging;
  int drag_offset_x;
  int drag_offset_y;
  Bool was_chasing;
  Bool was_frozen;
  const char *speech;
  long speech_time;
  Window bubble_window;
};

Config config;
struct mask_map {
  const char *name;
  unsigned long mask;
  const char *desc;
};
const struct mask_map MASK_MAP[] = {{"Mod1Mask", Mod1Mask, "Alt"},
                                    {"Mod2Mask", Mod2Mask, "NumLock"},
                                    {"Mod3Mask", Mod3Mask, "Mod3"},
                                    {"Mod4Mask", Mod4Mask, "Super"},
                                    {"Mod5Mask", Mod5Mask, "Mod5"},
                                    {"ShiftMask", ShiftMask, "Shift"},
                                    {"LockMask", LockMask, "CapsLock"},
                                    {"ControlMask", ControlMask, "Ctrl"},
                                    {NULL, 0, NULL}};
struct func_map {
  const char *name;
  KeyFunc func;
  const char *desc;
};
const struct func_map FUNC_MAP[] = {
    {"toggle_chase", FUNC_TOGGLE_CHASE, "Toggle chase mode"},
    {"toggle_freeze", FUNC_TOGGLE_FREEZE, "Toggle freeze state"},
    {"quit", FUNC_QUIT, "Quit program"},
    {"hello_world", FUNC_PRINT_HELLOWORLD, "Print hello world"},
    {"helper_info_of_program", FUNC_HELPER_INFO, "Show help info"},
    {"hot_reload", FUNC_HOT_RELOAD,
     "Hot reload config (no restart)"}, // 新增映射
    {NULL, FUNC_UNKNOWN, NULL}};
struct animation animations[] = {
    [HAPPY] = {.name = "happy", .n_frames = 0, .loop = True, .frames = NULL},
    [SLEEPING] = {.name = "sleeping",
                  .n_frames = 0,
                  .loop = True,
                  .frames = NULL},
    [IDLE] = {.name = "idle", .n_frames = 0, .loop = True, .frames = NULL},
    [DRAGGED] = {.name = "dragged",
                 .n_frames = 0,
                 .loop = True,
                 .frames = NULL},
    [N] = {.name = "walk_north", .n_frames = 0, .loop = True, .frames = NULL},
    [S] = {.name = "walk_south", .n_frames = 0, .loop = True, .frames = NULL},
    [E] = {.name = "walk_east", .n_frames = 0, .loop = True, .frames = NULL},
    [W] = {.name = "walk_west", .n_frames = 0, .loop = True, .frames = NULL},
    [NW] = {.name = "walk_northwest",
            .n_frames = 0,
            .loop = True,
            .frames = NULL},
    [NE] = {.name = "walk_northeast",
            .n_frames = 0,
            .loop = True,
            .frames = NULL},
    [SW] = {.name = "walk_southwest",
            .n_frames = 0,
            .loop = True,
            .frames = NULL},
    [SE] = {.name = "walk_southeast",
            .n_frames = 0,
            .loop = True,
            .frames = NULL},
};
Display *dpy;
Window root;
int scr;
int scr_width;
int scr_height;
XpmAttributes xpm_attrs;
FT_Library ft_lib;
FT_Face ft_face;
struct mouse mouse;
struct pet pet;

char *trim_whitespace(char *str) {
  if (!str)
    return NULL;
  while (*str && (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r'))
    str++;
  char *end = str + strlen(str) - 1;
  while (end > str &&
         (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r'))
    end--;
  *(end + 1) = '\0';
  return str;
}

char *expand_tilde(const char *path) {
  if (!path)
    return NULL;
  if (path[0] == '~') {
    char *home = getenv("HOME");
    if (!home)
      return strdup(path);
    char *expanded = malloc(strlen(home) + strlen(path) + 1);
    snprintf(expanded, strlen(home) + strlen(path) + 1, "%s%s", home, path + 1);
    return expanded;
  }
  return strdup(path);
}

int mkdir_p(const char *path, mode_t mode) {
  char dir[PATH_MAX];
  strncpy(dir, path, sizeof(dir));
  size_t len = strlen(dir);
  if (dir[len - 1] == '/')
    dir[len - 1] = '\0';
  char *p = dir + 1;
  while (*p) {
    if (*p == '/') {
      *p = '\0';
      if (mkdir(dir, mode) != 0 && errno != EEXIST)
        return -1;
      *p = '/';
    }
    p++;
  }
  return mkdir(dir, mode) != 0 && errno != EEXIST ? -1 : 0;
}

Bool is_audio_file(const char *filename) {
  const char *ext[] = {".mp3", ".wav", ".ogg", ".flac", NULL};
  for (int i = 0; ext[i]; i++) {
    if (strstr(filename, ext[i]) != NULL)
      return True;
  }
  return False;
}

char *random_audio_from_folder(const char *folder) {
  DIR *dir = opendir(folder);
  if (!dir) {
    fprintf(stderr, "Warning: Invalid random audio folder '%s'\n", folder);
    return NULL;
  }

  struct dirent *entry;
  char **audio_files = NULL;
  int file_count = 0;

  while ((entry = readdir(dir)) != NULL) {
    if (entry->d_type != DT_REG)
      continue;
    if (is_audio_file(entry->d_name)) {
      audio_files = realloc(audio_files, sizeof(char *) * (file_count + 1));
      char *file_path = malloc(PATH_MAX);
      snprintf(file_path, PATH_MAX, "%s/%s", folder, entry->d_name);
      audio_files[file_count++] = file_path;
    }
  }
  closedir(dir);

  if (file_count == 0) {
    fprintf(stderr, "Warning: No audio files in '%s'\n", folder);
    free(audio_files);
    return NULL;
  }
  int rand_idx = rand() % file_count;
  char *selected = strdup(audio_files[rand_idx]);

  for (int i = 0; i < file_count; i++)
    free(audio_files[i]);
  free(audio_files);

  return selected;
}

void play_click_audio(void) {
  if (!config.enable_audio)
    return;

  char *audio_path = NULL;

  if (config.enable_random_audio && config.random_audio_folder) {
    audio_path = random_audio_from_folder(config.random_audio_folder);
  }

  if (!audio_path || strlen(audio_path) == 0) {
    audio_path = strdup(config.click_audio_to_play);
  }

  if (!audio_path || strlen(audio_path) == 0 || access(audio_path, R_OK) != 0) {
    free(audio_path);
    return;
  }

  pid_t pid = fork();
  if (pid == 0) {
    execlp("ffplay", "ffplay", "-nodisp", "-autoexit", "-loglevel", "error",
           audio_path, NULL);
    exit(EXIT_FAILURE);
  } else if (pid < 0) {
    fprintf(stderr, "Warning: Failed to play audio\n");
  }

  free(audio_path);
}

void show_helper_info(Config *config) {
  show_speech_bubble("Help Example, I don't know your keybind\n"
                     "--------------------------------------\n"
                     "Alt+f: Toggle chase mode\n"
                     "Alt+s: Toggle freeze state\n"
                     "Alt+q: Quit program\n"
                     "Alt+p: Print hello world\n"
                     "Alt+h: Show help info\n"
                     "Alt+r: Hot reload config (no restart)\n"
                     "Right-click: Show phrase\n");
}

// 1. 修复 generate_default_config（通用路径 + 短语标点优化）
void generate_default_config(const char *config_path) {
  FILE *f = fopen(config_path, "w");
  if (!f) {
    perror("generate default config");
    return;
  }

  // Phrases（修正标点，让句子更通顺）
  fprintf(f, "phrase = \"Get off the chair, you lazybones~\"\n");
  fprintf(
      f,
      "phrase = \"Oh wow, you're too chubby—time to exercise~\"\n"); // 加破折号
  fprintf(f,
          "phrase = \"Woof! A stranger's approaching? I'll protect you!\"\n");
  fprintf(f, "phrase = \"Woof! Wanna play fetch? Throw the ball already~\"\n");
  fprintf(
      f,
      "phrase = \"The weather's great today—wanna go for a walk?\"\n"); // 加破折号
  fprintf(f, "phrase = \"Whimper~ I'm kinda hungry, need foodies!\"\n");
  fprintf(f, "phrase = \"Master, play with me please? I'm so bored~\"\n");
  fprintf(f, "phrase = \"I like you so much! I'll always follow you~\"\n\n");

  // Font settings
  fprintf(f,
          "font_path = \"/usr/share/fonts/truetype/freefont/FreeSans.ttf\"\n");
  fprintf(f, "font_size = 24\n\n");

  // Pet settings（scale_factor 默认2，pet_asset_dir 改为通用路径）
  fprintf(f, "scale_factor = 2\n");
  fprintf(f, "pet_speed = 2\n");
  fprintf(f, "frame_duration = 200\n");
  fprintf(
      f,
      "pet_asset_dir = \"~/.config/xpet/pets/dog\"\n\n"); // 通用路径，expand_tilde会自动解析~

  // Audio settings
  fprintf(f, "enable_audio = true\n");
  fprintf(f, "#click_audio_to_play = \"~/.config/xpet/audio/click.mp3\"\n");
  fprintf(f, "enable_random_audio = true\n");
  fprintf(f, "random_audio_folder = \"~/.config/xpet/audio/\"\n\n");

  // Keybinds（保持原有顺序，热重载键位在中间）
  fprintf(f, "keybind Mod1Mask+f toggle_chase\n");
  fprintf(f, "keybind Mod1Mask+s toggle_freeze\n");
  fprintf(f, "keybind Mod1Mask+q quit\n");
  fprintf(f, "keybind Mod1Mask+p hello_world\n");
  fprintf(f, "keybind Mod1Mask+r hot_reload\n"); // Alt+R 热重载
  fprintf(f, "keybind Mod1Mask+h helper_info_of_program\n");

  fclose(f);
  printf("Generated default config: %s\n", config_path);
}

unsigned long mask_str_to_mask(const char *mask_str) {
  for (int i = 0; MASK_MAP[i].name; i++)
    if (strcmp(mask_str, MASK_MAP[i].name) == 0)
      return MASK_MAP[i].mask;
  fprintf(stderr, "Error: Unknown modifier '%s'\n", mask_str);
  exit(1);
}

KeyFunc func_str_to_func(const char *func_str, const char **desc) {
  for (int i = 0; FUNC_MAP[i].name; i++)
    if (strcmp(func_str, FUNC_MAP[i].name) == 0) {
      *desc = FUNC_MAP[i].desc;
      return FUNC_MAP[i].func;
    }
  fprintf(stderr, "Error: Unknown function '%s'\n", func_str);
  exit(1);
}

void parse_key_str(const char *key_str, char **mask_str, char **sym_str) {
  char *plus_pos = strchr(key_str, '+');
  if (!plus_pos) {
    fprintf(stderr, "Error: Invalid keybind format '%s' (use Mod+Key)\n",
            key_str);
    exit(1);
  }
  *mask_str = strndup(key_str, plus_pos - key_str);
  *sym_str = strdup(plus_pos + 1);
}

int check_asset_dir(const char *dir) {
  struct stat st;
  return stat(dir, &st) == 0 && S_ISDIR(st.st_mode);
}

int check_font_file(const char *path) {
  struct stat st;
  return stat(path, &st) == 0 && S_ISREG(st.st_mode);
}

void parse_config(const char *config_path) {
  // Initialize all config values to default/empty
  config.phrase_count = 0;
  config.phrases = NULL;
  config.scale_factor = 0;
  config.pet_speed = 0;
  config.frame_duration = 0;
  config.pet_asset_dir = NULL;
  config.font_path = NULL;
  config.font_size = 0;
  config.keybinds = NULL;
  config.keybind_count = 0;
  config.click_audio_to_play = NULL;
  config.enable_audio = False;
  config.enable_random_audio = False;
  config.random_audio_folder = NULL;

  FILE *f = fopen(config_path, "r");
  if (!f) {
    perror("Error opening config file");
    exit(1);
  }

  char line[1024];
  while (fgets(line, sizeof(line), f)) {
    line[strcspn(line, "\n\r")] = '\0';
    if (line[0] == '\0')
      continue;
    if (line[0] == '#')
      continue; // Skip comments
    if (strspn(line, " \t") == strlen(line))
      continue; // Skip whitespace lines

    if (strstr(line, "keybind ") == line) {
      char *key_str = strtok(line + 7, " \t");
      char *func_str = strtok(NULL, " \t");
      if (!key_str || !func_str) {
        fprintf(stderr, "Error: Invalid keybind line: %s\n", line);
        continue;
      }

      char *mask_str = NULL;
      char *sym_str = NULL;
      parse_key_str(key_str, &mask_str, &sym_str);

      unsigned long mask = mask_str_to_mask(mask_str);
      KeySym sym = XStringToKeysym(sym_str);
      if (sym == NoSymbol) {
        fprintf(stderr, "Error: Unknown key '%s'\n", sym_str);
        free(mask_str);
        free(sym_str);
        continue;
      }

      const char *func_desc;
      KeyFunc func = func_str_to_func(func_str, &func_desc);

      config.keybinds = realloc(
          config.keybinds, sizeof(struct keybind) * (config.keybind_count + 1));
      config.keybinds[config.keybind_count] = (struct keybind){
          .sym = sym, .mask = mask, .func = func, .desc = strdup(func_desc)};
      config.keybind_count++;

      free(mask_str);
      free(sym_str);
      continue;
    }

    char *eq_pos = strchr(line, '=');
    if (!eq_pos) {
      fprintf(stderr, "Warning: Invalid config line (no '='): %s\n", line);
      continue;
    }

    *eq_pos = '\0';
    char *key = trim_whitespace(line);
    char *value = trim_whitespace(eq_pos + 1);

    if (value[0] == '"' && value[strlen(value) - 1] == '"') {
      value[strlen(value) - 1] = '\0';
      value++;
    }

    if (!key || !value || *key == '\0' || *value == '\0')
      continue;

    if (strcmp(key, "phrase") == 0) {
      config.phrases =
          realloc(config.phrases, sizeof(char *) * (config.phrase_count + 1));
      config.phrases[config.phrase_count++] = strdup(value);
    } else if (strcmp(key, "scale_factor") == 0) {
      config.scale_factor = atoi(value);
      if (config.scale_factor <= 0) {
        fprintf(stderr, "Error: scale_factor must be positive\n");
        exit(1);
      }
    } else if (strcmp(key, "pet_speed") == 0) {
      config.pet_speed = atoi(value);
      if (config.pet_speed <= 0) {
        fprintf(stderr, "Error: pet_speed must be positive\n");
        exit(1);
      }
    } else if (strcmp(key, "frame_duration") == 0) {
      config.frame_duration = atoi(value);
      if (config.frame_duration <= 0) {
        fprintf(stderr, "Error: frame_duration must be positive\n");
        exit(1);
      }
    } else if (strcmp(key, "pet_asset_dir") == 0) {
      config.pet_asset_dir = expand_tilde(value);
    } else if (strcmp(key, "font_path") == 0) {
      config.font_path = expand_tilde(value);
    } else if (strcmp(key, "font_size") == 0) {
      config.font_size = atoi(value);
      if (config.font_size <= 0) {
        fprintf(stderr, "Error: font_size must be positive\n");
        exit(1);
      }
    } else if (strcmp(key, "click_audio_to_play") == 0) {
      config.click_audio_to_play = expand_tilde(value);
    } else if (strcmp(key, "enable_audio") == 0) {
      config.enable_audio = (strcmp(value, "true") == 0) ? True : False;
    } else if (strcmp(key, "enable_random_audio") == 0) {
      config.enable_random_audio = (strcmp(value, "true") == 0) ? True : False;
    } else if (strcmp(key, "random_audio_folder") == 0) {
      config.random_audio_folder = expand_tilde(value);
    } else {
      fprintf(stderr, "Warning: Unknown config key: %s\n", key);
    }
  }
  fclose(f);

  // Validate required config values
  if (!config.pet_asset_dir || !check_asset_dir(config.pet_asset_dir)) {
    fprintf(stderr, "Error: Invalid pet_asset_dir: %s\n", config.pet_asset_dir);
    exit(1);
  }
  if (!config.font_path || !check_font_file(config.font_path)) {
    fprintf(stderr, "Error: Invalid font_path: %s\n", config.font_path);
    exit(1);
  }
  if (config.phrase_count == 0) {
    fprintf(stderr, "Error: No phrases defined in config\n");
    exit(1);
  }
  if (config.keybind_count == 0) {
    fprintf(stderr, "Error: No keybinds defined in config\n");
    exit(1);
  }
  if (config.enable_audio && config.enable_random_audio &&
      (!config.random_audio_folder ||
       !check_asset_dir(config.random_audio_folder))) {
    fprintf(
        stderr,
        "Error: Invalid random_audio_folder (enable_random_audio is true)\n");
    exit(1);
  }
}

void load_config(void) {
  char *config_dir = expand_tilde(CONFIG_DIR);
  char *config_path = expand_tilde(CONFIG_PATH);

  if (mkdir_p(config_dir, 0755) != 0) {
    perror("Error creating config directory");
    free(config_dir);
    free(config_path);
    exit(1);
  }

  struct stat st;
  if (stat(config_path, &st) != 0) {
    generate_default_config(config_path);
  }

  parse_config(config_path);

  free(config_dir);
  free(config_path);
}

Pixmap scale_pixmap(Display *dpy, Drawable src, int src_w, int src_h,
                    int scale) {
  int dest_w = src_w * scale;
  int dest_h = src_h * scale;
  Pixmap dest =
      XCreatePixmap(dpy, root, dest_w, dest_h, DefaultDepth(dpy, scr));
  GC gc = XCreateGC(dpy, dest, 0, NULL);
  XImage *src_img = XGetImage(dpy, src, 0, 0, src_w, src_h, AllPlanes, ZPixmap);
  if (src_img) {
    for (int y = 0; y < src_h; y++) {
      for (int x = 0; x < src_w; x++) {
        unsigned long pixel = XGetPixel(src_img, x, y);
        XSetForeground(dpy, gc, pixel);
        XFillRectangle(dpy, dest, gc, x * scale, y * scale, scale, scale);
      }
    }
    XDestroyImage(src_img);
  }
  XFreeGC(dpy, gc);
  return dest;
}

Pixmap scale_mask(Display *dpy, Drawable src, int src_w, int src_h, int scale) {
  int dest_w = src_w * scale;
  int dest_h = src_h * scale;
  Pixmap dest = XCreatePixmap(dpy, root, dest_w, dest_h, 1);
  GC gc = XCreateGC(dpy, dest, 0, NULL);
  XSetForeground(dpy, gc, 0);
  XImage *src_img = XGetImage(dpy, src, 0, 0, src_w, src_h, AllPlanes, ZPixmap);
  if (src_img) {
    for (int y = 0; y < src_h; y++) {
      for (int x = 0; x < src_w; x++) {
        unsigned long pixel = XGetPixel(src_img, x, y) & 1;
        XSetForeground(dpy, gc, pixel);
        XFillRectangle(dpy, dest, gc, x * scale, y * scale, scale, scale);
      }
    }
    XDestroyImage(src_img);
  }
  XFreeGC(dpy, gc);
  return dest;
}

void xsleep(long ms) {
  struct timeval tv = {ms / 1000, (int)((ms % 1000) * 1000)};
  select(0, 0, 0, 0, &tv);
}

Bool walking(enum state s) {
  return s == N || s == S || s == E || s == W || s == NW || s == NE ||
         s == SW || s == SE;
}

void step(enum state d, double *step_x, double *step_y) {
  const double s = config.pet_speed;
  const double ds = s * 0.7071067811865475;
  switch (d) {
  case E:
    *step_x = s;
    *step_y = 0;
    break;
  case W:
    *step_x = -s;
    *step_y = 0;
    break;
  case N:
    *step_x = 0;
    *step_y = -s;
    break;
  case S:
    *step_x = 0;
    *step_y = s;
    break;
  case NE:
    *step_x = ds;
    *step_y = -ds;
    break;
  case SE:
    *step_x = ds;
    *step_y = ds;
    break;
  case NW:
    *step_x = -ds;
    *step_y = -ds;
    break;
  case SW:
    *step_x = -ds;
    *step_y = ds;
    break;
  default:
    *step_x = 0;
    *step_y = 0;
    break;
  }
}

enum state find_octant(int delta_x, int delta_y) {
  int abs_delta_x = ABS(delta_x);
  int abs_delta_y = ABS(delta_y);
  if (abs_delta_x > abs_delta_y * 2)
    return delta_x > 0 ? E : W;
  if (abs_delta_y > abs_delta_x * 2)
    return delta_y > 0 ? S : N;
  return delta_x > 0 ? (delta_y < 0 ? NE : SE) : (delta_y < 0 ? NW : SW);
}

void hide_speech_bubble(void) {
  if (pet.bubble_window)
    XUnmapWindow(dpy, pet.bubble_window);
  pet.speech = NULL;
  pet.speech_time = 0;
}

void get_mouse_pos(void) {
  Window root_ret, child_ret;
  int x_ret, y_ret, winx_ret, winy_ret;
  unsigned int mask_ret;
  XQueryPointer(dpy, root, &root_ret, &child_ret, &x_ret, &y_ret, &winx_ret,
                &winy_ret, &mask_ret);
  mouse.x = x_ret;
  mouse.y = y_ret;
}

int init_freetype() {
  if (FT_Init_FreeType(&ft_lib) != 0) {
    fprintf(stderr, "Failed to initialize FreeType2\n");
    return -1;
  }
  if (FT_New_Face(ft_lib, config.font_path, 0, &ft_face) != 0) {
    fprintf(stderr, "Failed to load font: %s\n", config.font_path);
    FT_Done_FreeType(ft_lib);
    return -1;
  }
  FT_Set_Pixel_Sizes(ft_face, 0, config.font_size);
  return 0;
}

// Improved text extent calculation for better bubble sizing
void ft_get_text_extents(const char *text, int *width, int *height) {
  *width = 0;
  *height = 0;
  FT_GlyphSlot slot = ft_face->glyph;
  int max_ascent = 0, max_descent = 0;
  int current_width = 0;
  char *line = strdup(text);
  char *token = strtok(line, "\n");

  while (token) {
    current_width = 0;
    for (size_t i = 0; i < strlen(token); i++) {
      FT_UInt glyph_index = FT_Get_Char_Index(ft_face, (FT_ULong)token[i]);
      if (FT_Load_Glyph(ft_face, glyph_index, FT_LOAD_DEFAULT) != 0)
        continue;
      if (FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL) != 0)
        continue;

      current_width += slot->advance.x >> 6;
      max_ascent = MAX(max_ascent, slot->bitmap_top);
      max_descent = MAX(max_descent, slot->bitmap.rows - slot->bitmap_top);
    }
    *width = MAX(*width, current_width);
    *height += (max_ascent + max_descent) + 8; // Increased line spacing
    token = strtok(NULL, "\n");
  }

  free(line);
  *height = MAX(*height, max_ascent + max_descent);
}

void ft_draw_text(Window win, GC gc, const char *text, int x, int y) {
  FT_GlyphSlot slot = ft_face->glyph;
  int pen_x = x;
  int pen_y = y;
  int line_height =
      (ft_face->size->metrics.ascender - ft_face->size->metrics.descender) >> 6;

  XSetForeground(dpy, gc, BlackPixel(dpy, scr));

  char *line = strdup(text);
  if (!line)
    return;

  char *token = strtok(line, "\n");
  while (token) {
    pen_x = x;
    for (size_t i = 0; i < strlen(token); i++) {
      FT_ULong char_code = (FT_ULong)token[i];
      FT_UInt glyph_index = FT_Get_Char_Index(ft_face, char_code);
      if (FT_Load_Glyph(ft_face, glyph_index, FT_LOAD_DEFAULT) != 0) {
        // Fallback for unsupported characters (draw a placeholder)
        XDrawRectangle(dpy, win, gc, pen_x, pen_y - config.font_size / 2, 5, 5);
        pen_x += 8;
        continue;
      }
      if (FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL) != 0) {
        pen_x += 8;
        continue;
      }

      int x_pos = pen_x + slot->bitmap_left;
      int y_pos = pen_y - slot->bitmap_top;

      // Draw each pixel of the glyph
      for (int row = 0; row < slot->bitmap.rows; row++) {
        for (int col = 0; col < slot->bitmap.width; col++) {
          if (slot->bitmap.buffer[row * slot->bitmap.width + col] > 128) {
            XDrawPoint(dpy, win, gc, x_pos + col, y_pos + row);
          }
        }
      }

      pen_x += slot->advance.x >> 6;
    }
    pen_y += line_height + 8; // Increased line spacing
    token = strtok(NULL, "\n");
  }

  free(line);
}

void quit(void) {
  for (int i = 0; i < config.phrase_count; i++)
    free(config.phrases[i]);
  free(config.phrases);

  free(config.pet_asset_dir);
  free(config.font_path);
  free(config.click_audio_to_play);
  free(config.random_audio_folder);

  for (int i = 0; i < config.keybind_count; i++)
    free(config.keybinds[i].desc);
  free(config.keybinds);

  if (pet.bubble_window)
    XDestroyWindow(dpy, pet.bubble_window);
  for (int i = 0; i < STATE_LAST; i++) {
    if (animations[i].frames) {
      for (int j = 0; j < animations[i].n_frames; j++) {
        XFreePixmap(dpy, animations[i].frames[j].pix);
        XFreePixmap(dpy, animations[i].frames[j].mask);
      }
      free(animations[i].frames);
    }
  }

  if (ft_face)
    FT_Done_Face(ft_face);
  if (ft_lib)
    FT_Done_FreeType(ft_lib);

  XCloseDisplay(dpy);
  exit(EXIT_SUCCESS);
}

void pick_random_destination(void) {
  int min_x = DEFAULT_WANDER_MARGIN;
  int min_y = DEFAULT_WANDER_MARGIN;
  int scaled_size = DEFAULT_ORIGINAL_SIZE * config.scale_factor;
  int max_x = scr_width - DEFAULT_WANDER_MARGIN - scaled_size;
  int max_y = scr_height - DEFAULT_WANDER_MARGIN - scaled_size;
  if (max_x < min_x)
    max_x = min_x;
  if (max_y < min_y)
    max_y = min_y;
  pet.target_x = min_x + (rand() % (max_x - min_x + 1));
  pet.target_y = min_y + (rand() % (max_y - min_y + 1));
  pet.wander_wait = 0;
}

void set_pet_state(enum state new_state) {
  if (pet.state != new_state) {
    if (walking(pet.state) && walking(new_state)) {
      int current_frame = pet.current_frame;
      long frame_time = pet.frame_time;
      pet.state = new_state;
      pet.current_animation = &animations[new_state];
      if (pet.current_animation->n_frames > 0) {
        pet.current_frame = current_frame % pet.current_animation->n_frames;
        int dur = pet.current_animation->frames[pet.current_frame].duration;
        pet.frame_time = (frame_time < dur) ? frame_time : 0;
      } else {
        pet.current_frame = pet.frame_time = 0;
      }
    } else {
      pet.state = new_state;
      pet.current_animation = &animations[new_state];
      pet.current_frame = 0;
      pet.frame_time = 0;
    }
  } else if (pet.current_animation != &animations[new_state]) {
    pet.current_animation = &animations[new_state];
    if (!walking(new_state)) {
      pet.current_frame = pet.frame_time = 0;
    } else if (pet.current_animation->n_frames > 0) {
      pet.current_frame %= pet.current_animation->n_frames;
      int duration = pet.current_animation->frames[pet.current_frame].duration;
      if (pet.frame_time >= duration)
        pet.frame_time = 0;
    }
  }
}

// 3. 修改 draw_bubble（文字在放大窗口中居中）
void draw_bubble(void) {
  if (!pet.speech || !pet.bubble_window)
    return;
  XClearWindow(dpy, pet.bubble_window);
  GC gc = DefaultGC(dpy, scr);

  // 获取放大后的窗口实际尺寸
  Window root_win;
  int x, y;
  unsigned int width, height, border, depth;
  XGetGeometry(dpy, pet.bubble_window, &root_win, &x, &y, &width, &height,
               &border, &depth);

  // 绘制背景和边框（保持不变）
  XSetForeground(dpy, gc, WhitePixel(dpy, scr));
  XFillRectangle(dpy, pet.bubble_window, gc, 0, 0, width, height);
  XSetForeground(dpy, gc, BlackPixel(dpy, scr));
  XDrawRectangle(dpy, pet.bubble_window, gc, 0, 0, width - 1, height - 1);

  // 计算文字居中位置（关键：在放大窗口中水平+垂直居中）
  int text_width, text_height;
  ft_get_text_extents(pet.speech, &text_width, &text_height);
  int text_x = (width - text_width) / 2; // 水平居中
  int text_y = (height - text_height) / 2 +
               config.font_size; // 垂直居中（加字体高度避免上移）

  // 绘制居中文字
  ft_draw_text(pet.bubble_window, gc, pet.speech, text_x, text_y);
}

// 2. 修改 show_speech_bubble（核心：放大窗口尺寸+居中定位）
void show_speech_bubble(const char *s) {
  if (!s)
    return;
  pet.speech = s;
  pet.speech_time = 0;

  if (!pet.bubble_window) {
    XSetWindowAttributes attr;
    attr.override_redirect = True;
    attr.background_pixel = WhitePixel(dpy, scr);
    attr.border_pixel = BlackPixel(dpy, scr);
    pet.bubble_window = XCreateWindow(
        dpy, root, 0, 0, 10, 10, 2, CopyFromParent, InputOutput, CopyFromParent,
        CWOverrideRedirect | CWBackPixel | CWBorderPixel, &attr);
    XSelectInput(dpy, pet.bubble_window, ExposureMask);
  }

  int text_width, text_height;
  ft_get_text_extents(s, &text_width, &text_height);

  int scaled_size = DEFAULT_ORIGINAL_SIZE * config.scale_factor;
  // 核心：窗口宽高 = （文本尺寸 + 双边padding）× 缩放系数，再限制最大尺寸
  int bw = MIN((text_width + BUBBLE_PADDING * 2) * HELPER_BUBBLE_SCALE,
               MAX_BUBBLE_WIDTH);
  int bh = MIN((text_height + BUBBLE_PADDING * 2) * HELPER_BUBBLE_SCALE,
               MAX_BUBBLE_HEIGHT);

  // 窗口居中在宠物上方（避免超出屏幕）
  int bx = CLAMP(pet.x + (scaled_size / 2) - (bw / 2), 10, scr_width - 10 - bw);
  int by =
      CLAMP(pet.y - bh - 20, 10, scr_height - 10 - bh); // 下移20px，避免贴宠物

  XMoveResizeWindow(dpy, pet.bubble_window, bx, by, bw, bh);
  XMapWindow(dpy, pet.bubble_window);
  draw_bubble();
  XRaiseWindow(dpy, pet.bubble_window);
}

void load_animations(void) {
  memset(&xpm_attrs, 0, sizeof(xpm_attrs));
  xpm_attrs.valuemask = XpmSize;
  printf("Loading animations from: %s\n", config.pet_asset_dir);

  for (int i = 0; i < STATE_LAST; i++) {
    int frame_count = 0;
    char path[PATH_MAX];
    for (;;) {
      snprintf(path, sizeof(path), "%s/%s/%d.xpm", config.pet_asset_dir,
               animations[i].name, frame_count);
      FILE *f = fopen(path, "r");
      if (!f)
        break;
      fclose(f);
      frame_count++;
    }
    if (frame_count == 0) {
      fprintf(stderr, "Error: No frames for animation '%s'\n",
              animations[i].name);
      exit(1);
    }

    animations[i].frames = malloc(sizeof(struct frame) * frame_count);
    if (!animations[i].frames) {
      perror("malloc failed");
      exit(1);
    }

    for (int frame_index = 0; frame_index < frame_count; frame_index++) {
      snprintf(path, sizeof(path), "%s/%s/%d.xpm", config.pet_asset_dir,
               animations[i].name, frame_index);
      Pixmap pixmap = 0, mask = 0;
      int xpm_result =
          XpmReadFileToPixmap(dpy, root, path, &pixmap, &mask, &xpm_attrs);
      if (xpm_result != XpmSuccess) {
        fprintf(stderr, "Error loading animation frame: %s\n", path);
        exit(1);
      }

      Pixmap scaled_pix = scale_pixmap(dpy, pixmap, xpm_attrs.width,
                                       xpm_attrs.height, config.scale_factor);
      Pixmap scaled_mask = scale_mask(dpy, mask, xpm_attrs.width,
                                      xpm_attrs.height, config.scale_factor);
      XFreePixmap(dpy, pixmap);
      XFreePixmap(dpy, mask);

      animations[i].frames[frame_index] =
          (struct frame){.pix = scaled_pix,
                         .mask = scaled_mask,
                         .duration = config.frame_duration};
    }
    animations[i].n_frames = frame_count;
  }
}

void create_window(void) {
  pet.x = scr_width / 2;
  pet.y = scr_height / 2;
  pet.subpixel_x = pet.x;
  pet.subpixel_y = pet.y;
  int scaled_size = DEFAULT_ORIGINAL_SIZE * config.scale_factor;

  XSetWindowAttributes attrs;
  attrs.override_redirect = True;
  pet.window = XCreateWindow(dpy, root, pet.x, pet.y, scaled_size, scaled_size,
                             0, CopyFromParent, InputOutput, CopyFromParent,
                             CWOverrideRedirect, &attrs);

  struct frame *f = &pet.current_animation->frames[0];
  XShapeCombineMask(dpy, pet.window, ShapeBounding, 0, 0, f->mask, ShapeSet);
  XSetWindowBackgroundPixmap(dpy, pet.window, f->pix);
  XSelectInput(dpy, pet.window,
               ButtonPressMask | ButtonReleaseMask | PointerMotionMask);
  XMapWindow(dpy, pet.window);
  XClearWindow(dpy, pet.window);
  XRaiseWindow(dpy, pet.window);
}

// Modified grab_keys function to fix key grab warnings
void grab_keys(void) {
  // Only include LockMask (CapsLock) as additional modifier - avoids NumLock
  // conflicts
  unsigned int mods[] = {0, LockMask};
  for (int i = 0; i < config.keybind_count; i++) {
    KeySym sym = config.keybinds[i].sym;
    unsigned long base_mask = config.keybinds[i].mask;
    KeyCode code = XKeysymToKeycode(dpy, sym);
    if (code == NoSymbol) {
      fprintf(stderr, "Error: Could not get keycode for %s\n",
              XKeysymToString(sym));
      continue;
    }
    // Only grab combinations with base mask + 0 or base mask + CapsLock
    for (unsigned j = 0; j < sizeof(mods) / sizeof(mods[0]); j++) {
      if (XGrabKey(dpy, code, base_mask | mods[j], root, True, GrabModeAsync,
                   GrabModeAsync) != Success) {
        // just ignore ?
        // fprintf(stderr, "Warning: Could not grab key %s+%s\n",
        //         XKeysymToString(sym),
        //         (mods[j] == LockMask) ? "Mod1Mask+CapsLock" : "Mod1Mask");
      }
    }
  }
}

void hot_reload(void) {
  printf("Starting hot reload...\n");

  // 关键：记录旧配置和状态（用于保持位置和状态不变）
  int old_scale_factor = config.scale_factor;
  int old_scaled_size = DEFAULT_ORIGINAL_SIZE * old_scale_factor;
  int old_center_x = pet.x + old_scaled_size / 2; // 记录旧窗口中心X
  int old_center_y = pet.y + old_scaled_size / 2; // 记录旧窗口中心Y
  Bool old_chasing = pet.chasing;
  Bool old_frozen = pet.frozen;
  enum state old_state = pet.state;

  // 第一步：释放旧配置的动态内存（避免内存泄漏）
  for (int i = 0; i < config.phrase_count; i++)
    free(config.phrases[i]);
  free(config.phrases);
  for (int i = 0; i < config.keybind_count; i++)
    free(config.keybinds[i].desc);
  free(config.keybinds);
  free(config.pet_asset_dir);
  free(config.font_path);
  free(config.click_audio_to_play);
  free(config.random_audio_folder);

  // 第二步：重新初始化配置结构体
  config.phrase_count = 0;
  config.phrases = NULL;
  config.scale_factor = 0;
  config.keybind_count = 0;
  config.keybinds = NULL;
  config.pet_asset_dir = NULL;
  config.font_path = NULL;
  config.click_audio_to_play = NULL;
  config.random_audio_folder = NULL;

  // 第三步：重新解析配置文件（加载新的 scale_factor）
  char *config_path = expand_tilde(CONFIG_PATH);
  parse_config(config_path);
  free(config_path);

  // 第四步：更新依赖配置的运行时资源
  // 重新加载字体
  if (ft_face)
    FT_Done_Face(ft_face);
  if (FT_New_Face(ft_lib, config.font_path, 0, &ft_face) != 0) {
    fprintf(stderr, "Hot reload warning: Failed to reload font %s\n",
            config.font_path);
  } else {
    FT_Set_Pixel_Sizes(ft_face, 0, config.font_size);
  }

  // 重新加载动画（基于新的 scale_factor 缩放帧）
  for (int i = 0; i < STATE_LAST; i++) {
    if (animations[i].frames) {
      for (int j = 0; j < animations[i].n_frames; j++) {
        XFreePixmap(dpy, animations[i].frames[j].pix);
        XFreePixmap(dpy, animations[i].frames[j].mask);
      }
      free(animations[i].frames);
      animations[i].frames = NULL;
      animations[i].n_frames = 0;
    }
  }
  load_animations();

  // 重新抓取快捷键
  XUngrabKey(dpy, AnyKey, AnyModifier, root);
  grab_keys();

  // 关键：应用新的 scale_factor - 重新创建宠物窗口
  int new_scaled_size = DEFAULT_ORIGINAL_SIZE * config.scale_factor;
  XDestroyWindow(dpy, pet.window); // 销毁旧窗口
  create_window(); // 新窗口会使用当前（新的）scale_factor 计算大小

  // 保持窗口中心位置不变（用户无感知）
  pet.x = old_center_x - new_scaled_size / 2;
  pet.y = old_center_y - new_scaled_size / 2;
  pet.subpixel_x = pet.x;
  pet.subpixel_y = pet.y;
  XMoveWindow(dpy, pet.window, pet.x, pet.y); // 移动到原中心位置

  // 恢复原有状态（追逐/冻结/动画状态）
  pet.chasing = old_chasing;
  pet.frozen = old_frozen;
  set_pet_state(old_state);

  // 重新设置窗口背景和形状掩码（确保新动画帧生效）
  struct frame *new_frame = &pet.current_animation->frames[pet.current_frame];
  XShapeCombineMask(dpy, pet.window, ShapeBounding, 0, 0, new_frame->mask,
                    ShapeSet);
  XSetWindowBackgroundPixmap(dpy, pet.window, new_frame->pix);
  XClearWindow(dpy, pet.window);
  XRaiseWindow(dpy, pet.window);

  // 重新计算目标位置（避免新窗口超出屏幕）
  pick_random_destination();

  printf("Hot reload success! config now:\n");
  printf("  Basic settings:\n");
  printf("    scale_factor: %d\n", config.scale_factor);
  printf("    pet_speed: %d\n", config.pet_speed);
  printf("    frame_duration: %dms\n", config.frame_duration);

  printf("  Phrases (%d total):\n", config.phrase_count);
  for (int i = 0; i < config.phrase_count; i++) {
    printf("    [%d]: %s\n", i, config.phrases[i] ? config.phrases[i] : "NULL");
  }

  printf("  Font settings:\n");
  printf("    font_path: %s\n", config.font_path ? config.font_path : "NULL");
  printf("    font_size: %d\n", config.font_size);

  printf("  Asset paths:\n");
  printf("    pet_asset_dir: %s\n",
         config.pet_asset_dir ? config.pet_asset_dir : "NULL");
  printf("    random_audio_folder: %s\n",
         config.random_audio_folder ? config.random_audio_folder : "NULL");
  printf("    click_audio_to_play: %s\n",
         config.click_audio_to_play ? config.click_audio_to_play : "NULL");

  printf("  Audio flags:\n");
  printf("    enable_audio: %s\n", config.enable_audio ? "true" : "false");
  printf("    enable_random_audio: %s\n",
         config.enable_random_audio ? "true" : "false");

  printf("  Keybinds (%d total):\n", config.keybind_count);
  for (int i = 0; i < config.keybind_count; i++) {
    const char *mask_desc = "Unknown";
    for (int j = 0; MASK_MAP[j].name; j++) {
      if (MASK_MAP[j].mask == config.keybinds[i].mask) {
        mask_desc = MASK_MAP[j].desc;
        break;
      }
    }
    const char *key = XKeysymToString(config.keybinds[i].sym);
    printf("    [%d]: %s+%s → %s\n", i, mask_desc, key ? key : "UnknownKey",
           config.keybinds[i].desc ? config.keybinds[i].desc : "Unknown");
  }
  show_speech_bubble("Hot reload success!");
}

void setup(void) {
  load_config();

  dpy = XOpenDisplay(NULL);
  if (!dpy) {
    fputs("Cannot open display\n", stderr);
    exit(1);
  }
  scr = DefaultScreen(dpy);
  scr_width = DisplayWidth(dpy, scr);
  scr_height = DisplayHeight(dpy, scr);
  root = RootWindow(dpy, scr);

  if (init_freetype() != 0) {
    fprintf(stderr, "Fatal: FreeType2 initialization failed\n");
    exit(1);
  }

  srand((unsigned)time(NULL));
  load_animations();
  pet.current_frame = 0;
  pet.frame_time = 0;
  pet.frozen_time = 0;
  pet.happy_time = 0;
  pet.previous_state = IDLE;
  pet.dragging = False;
  pet.was_chasing = False;
  pet.was_frozen = False;
  pet.speech = NULL;
  pet.speech_time = 0;
  pet.bubble_window = 0;
  pet.chasing = False;
  pet.frozen = False;
  set_pet_state(IDLE);
  pick_random_destination();
  create_window();
  grab_keys();
  XSelectInput(dpy, root, KeyPressMask);
}

void execute_key_func(KeyFunc func) {
  switch (func) {
  case FUNC_TOGGLE_CHASE:
    pet.chasing = !pet.chasing;
    if (pet.chasing) {
      pet.frozen = False;
      set_pet_state(E);
    } else {
      pick_random_destination();
    }
    break;
  case FUNC_TOGGLE_FREEZE:
    pet.frozen = !pet.frozen;
    if (pet.frozen) {
      set_pet_state(IDLE);
      pet.frozen_time = 0;
    }
    break;
  case FUNC_QUIT:
    quit();
    break;
  case FUNC_PRINT_HELLOWORLD:
    pet.frozen = true;
    show_speech_bubble("hello world");
    break;
  case FUNC_HOT_RELOAD:
    pet.frozen = true;
    hot_reload();
    break;
  case FUNC_HELPER_INFO:
    pet.frozen = true;
    if (pet.frozen) {
      set_pet_state(IDLE);
      pet.frozen_time = 0;
    }
    show_helper_info(NULL);
    break;
  default:
    break;
  }
}

void on_key(XKeyEvent *ev) {
  KeySym sym = XLookupKeysym(ev, 0);
  unsigned long pressed_mask = ev->state & ~(LockMask | Mod2Mask);

  for (int i = 0; i < config.keybind_count; i++) {
    if (config.keybinds[i].sym == sym &&
        config.keybinds[i].mask == pressed_mask) {
      execute_key_func(config.keybinds[i].func);
      break;
    }
  }
}

void on_button_press(XButtonEvent *b) {
  play_click_audio();

  if (b->button == Button1) {
    pet.was_chasing = pet.chasing;
    pet.was_frozen = pet.frozen;
    pet.dragging = True;
    pet.drag_offset_x = b->x;
    pet.drag_offset_y = b->y;
    pet.frozen = True;
    pet.chasing = False;
    set_pet_state(DRAGGED);
  } else if (b->button == Button3) {
    if (pet.state != HAPPY) {
      pet.previous_state = pet.state;
      set_pet_state(HAPPY);
      pet.happy_time = 0;
      int rand_idx = rand() % config.phrase_count;
      show_speech_bubble(config.phrases[rand_idx]);
    }
  }
}

void on_button_release(XButtonEvent *b) {
  if (b->button != Button1)
    return;
  pet.dragging = False;
  pet.frozen = pet.was_frozen;
  pet.chasing = pet.was_chasing;
  pet.subpixel_x = pet.x;
  pet.subpixel_y = pet.y;
  if (pet.was_frozen) {
    set_pet_state(IDLE);
    pet.frozen_time = 0;
  } else if (!pet.was_chasing) {
    pick_random_destination();
    set_pet_state(IDLE);
  } else {
    set_pet_state(E);
  }
}

void on_motion(XMotionEvent *m) {
  (void)m;
  if (!pet.dragging)
    return;
  get_mouse_pos();
  pet.x = mouse.x - pet.drag_offset_x;
  pet.y = mouse.y - pet.drag_offset_y;
  XMoveWindow(dpy, pet.window, pet.x, pet.y);
  if (pet.speech)
    show_speech_bubble(pet.speech);
}

void move_to(int target_x, int target_y) {
  double delta_x = target_x - pet.subpixel_x;
  double delta_y = target_y - pet.subpixel_y;
  double distance_squared = delta_x * delta_x + delta_y * delta_y;
  if (distance_squared < 4.0) {
    set_pet_state(IDLE);
    return;
  }

  enum state direction = find_octant((int)delta_x, (int)delta_y);
  double step_x = 0, step_y = 0;
  step(direction, &step_x, &step_y);

  if (distance_squared <= (config.pet_speed * config.pet_speed)) {
    pet.subpixel_x = target_x;
    pet.subpixel_y = target_y;
  } else {
    pet.subpixel_x += step_x;
    pet.subpixel_y += step_y;
  }

  pet.x = (int)(pet.subpixel_x + 0.5);
  pet.y = (int)(pet.subpixel_y + 0.5);
  set_pet_state(direction);
  XMoveWindow(dpy, pet.window, pet.x, pet.y);
}

void wander(void) {
  double delta_x = pet.target_x - pet.subpixel_x;
  double delta_y = pet.target_y - pet.subpixel_y;
  double distance_squared = delta_x * delta_x + delta_y * delta_y;

  if (distance_squared < 4.0) {
    if (pet.wander_wait <= 0) {
      pet.wander_wait =
          DEFAULT_WANDER_MIN_WAIT +
          (rand() % (DEFAULT_WANDER_MAX_WAIT - DEFAULT_WANDER_MIN_WAIT));
      set_pet_state(IDLE);
    } else {
      pet.wander_wait -= DEFAULT_PET_REFRESH;
      if (pet.wander_wait <= 0)
        pick_random_destination();
    }
    return;
  }

  move_to(pet.target_x, pet.target_y);
}

void update_animation(void) {
  if (!pet.current_animation || pet.current_animation->n_frames <= 0) {
    if (pet.state != IDLE)
      set_pet_state(IDLE);
    return;
  }

  pet.frame_time += DEFAULT_PET_REFRESH;
  struct frame *frame_ptr = &pet.current_animation->frames[pet.current_frame];
  if (pet.frame_time >= frame_ptr->duration) {
    pet.frame_time = 0;
    pet.current_frame++;
    if (pet.current_frame >= pet.current_animation->n_frames) {
      pet.current_frame =
          pet.current_animation->loop ? 0 : pet.current_animation->n_frames - 1;
    }
    frame_ptr = &pet.current_animation->frames[pet.current_frame];
    XShapeCombineMask(dpy, pet.window, ShapeBounding, 0, 0, frame_ptr->mask,
                      ShapeSet);
    XSetWindowBackgroundPixmap(dpy, pet.window, frame_ptr->pix);
    XClearWindow(dpy, pet.window);
  }
}

void run(void) {
  for (;;) {
    while (XPending(dpy)) {
      XEvent ev;
      XNextEvent(dpy, &ev);
      if (ev.type == KeyPress)
        on_key(&ev.xkey);
      else if (ev.type == ButtonPress && ev.xbutton.window == pet.window)
        on_button_press(&ev.xbutton);
      else if (ev.type == ButtonRelease && ev.xbutton.window == pet.window)
        on_button_release(&ev.xbutton);
      else if (ev.type == MotionNotify && ev.xmotion.window == pet.window)
        on_motion(&ev.xmotion);
      else if (ev.type == Expose && ev.xexpose.window == pet.bubble_window)
        draw_bubble();
    }

    if (pet.speech) {
      pet.speech_time += DEFAULT_PET_REFRESH;
      if (pet.speech_time >= DEFAULT_SPEECH_DURATION)
        hide_speech_bubble();
    }

    if (pet.state == HAPPY) {
      pet.happy_time += DEFAULT_PET_REFRESH;
      if (pet.happy_time >= DEFAULT_HAPPY_DURATION)
        set_pet_state(pet.previous_state);
    }

    else if (!pet.dragging && !pet.frozen) {
      if (pet.chasing) {
        get_mouse_pos();
        move_to(mouse.x, mouse.y);
      } else {
        wander();
      }
    }

    else if (pet.frozen) {
      pet.frozen_time += DEFAULT_PET_REFRESH;
      if (pet.frozen_time >= DEFAULT_SLEEP_DELAY && pet.state != SLEEPING) {
        set_pet_state(SLEEPING);
      }
    }

    update_animation();

    XRaiseWindow(dpy, pet.window);
    if (pet.bubble_window)
      XRaiseWindow(dpy, pet.bubble_window);

    xsleep(DEFAULT_PET_REFRESH);
  }
}
int main(int argc, char **argv) {
  // 有参数时，仅处理帮助/版本，不运行程序
  if (argc > 1) {
    // 处理 -h/--help 帮助
    if (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0) {
      printf("xpets - 桌面互动宠物程序\n"
             "========================\n"
             "用法: xpet [选项]\n\n"
             "命令行选项:\n"
             "  -h, --help      显示完整帮助信息并退出\n"
             "  -v, --version   显示版本信息并退出\n\n"
             "核心功能:\n"
             "  • 左键拖拽: 移动宠物位置\n"
             "  • 右键点击: 触发宠物开心动画+随机短语气泡\n"
             "  • 快捷键操作: 查看配置文件中的键位绑定（默认 Alt+h "
             "显示内置帮助）\n\n"
             "配置文件:\n"
             "  路径: ~/.config/xpet/config\n"
             "  支持自定义: 宠物素材、短语、字体、音频、快捷键等\n\n"
             "依赖要求:\n"
             "  • 系统依赖: libX11, libXpm, FreeType2, ffmpeg(音频播放)\n"
             "  • 安装依赖: sudo apt install libx11-dev libxpm-dev "
             "libfreetype6-dev ffmpeg\n");
      return 0;
    }
    // 处理 -v/--version 版本
    else if (strcmp(argv[1], "-v") == 0 || strcmp(argv[1], "--version") == 0) {
      printf("xpet %s\n", VERSION);
      return 0;
    }
    // 未知参数提示
    else {
      fprintf(stderr, "错误: 未知参数 '%s'\n", argv[1]);
      fprintf(stderr, "使用 'xpet -h' 查看可用选项\n");
      return 1;
    }
  }

  // 无参数时，直接运行程序
  setup();
  run();
  return 0;
}
